package model;

/**
 * This class is a base for creating any type of game that uses a grid
 * (i.e. chess, tic-tac-toe, checkers, etc.)
 * @author Sacha Best
 */
public class GridGame<E>
{
	/**
	 * Instance variables for the rows, columns, number of total available spots,
	 * and number of occupied spots.
	 */
	private int rows, cols;
	/**
	 * The grid itself (a 2D array of Integers). 
	 */
	protected E[][] grid;
	
	/**
	 * Creates a new GridGame object. 
	 * @param rows - the number of rows in the grid
	 * @param cols - the number of columns in the grid
	 */
	@SuppressWarnings("unchecked")
	public GridGame(int rows, int cols)
	{
		this.rows = rows;
		this.cols = cols;
		grid = (E[][]) new Object[rows][cols];
	}
	/**
	 * @return the number of rows in the grid.
	 */
	public int getRows()
	{
		return rows;
	}
	/**
	 * @return - the number of columns in the grid.
	 */
	public int getCols()
	{
		return cols;
	}
	/**
	 * Checks if the Location loc is within the bounds of the grid.
	 * i.e. 0 =< row < rows and 0 <= col < columns.
	 * @param loc - the Location to be checked
	 * @return - true if loc is in bounds, false otherwise.
	 */
	public boolean inBounds(Location loc)
	{
		return (loc.getRow() >= 0 && loc.getRow() < rows && loc.getCol() >= 0 && loc.getCol() < cols);
	}
	/**
	 * Calls inBounds to check if the Location loc is within the grid.
	 * If it is not, an ArrayIndexOutOfBoundsException is thrown.
	 * Then returns the Integer at the given Location loc.
	 * @param loc - the Location to be checked.
	 * @return the object at the given Location loc.
	 */
	public E getObjectAt(Location loc)
	{
		if (!inBounds(loc))
			throw new ArrayIndexOutOfBoundsException("You entered values not in the array");
		return grid[loc.getRow()][loc.getCol()];
	}
	/**
	 * Moves an object in one Location to another, forcibly replacing what may
	 * or may not be in the "to" Location. Returns false if either Location is out
	 * of bounds, true otherwise.
	 * @param from - to Location to move.
	 * @param to - the Location moved to.
	 * @return false if either Location is out of bounds, true otherwise.
	 */
	public boolean move(Location from, Location to)
	{
		if (!inBounds(from) || !inBounds(to))
			return false;
		grid[to.getRow()][to.getCol()] = grid[from.getRow()][from.getCol()];
		grid[from.getRow()][from.getCol()] = null;
		return true;
	}
	/**
	 * Adds the given Integer to the grid at Location loc.
	 * @param addedObject - the object added to the grid.
	 * @param loc - the Location for the object to be added.
	 * @return a String representing the two possible cases of
	 * adding an object: a move out of bounds, and
	 * a normal add.
	 */
	public String add(E addedObject, Location loc)
	{
		if (!inBounds(loc))
			return "Out of Bounds";
		grid[loc.getRow()][loc.getCol()] = addedObject;
		return "Added";
	}
	/**
	 * This method provides another way to set an Object to a Location
	 * on the grid. Though the code is very similar to the add method,
	 * the add method is often overriden, thus the set method provides
	 * a constant, accessible method.
	 * @param object - the Object to be set
	 * @param loc - the Location at which to set the Object
	 * @return - "Out of Bounds" or "Set"
	 */
	public final String set(E object, Location loc)
	{
		if (!inBounds(loc))
			return "Out of Bounds";
		grid[loc.getRow()][loc.getCol()] = object;
		return "Set";
	}
	/**
	 * THis method determines whether or note {@link model.Location} toCheck is in between {@link model.Location}
	 * loc1 and loc2. It first calls {@link GridGame<E>.inSamePlane to check if the {@link model.Location}s arer in
	 * the same row, column, or diagonal. Then, it compares the values for row and col in each {@link model.Location}
	 * object to determine if toCheck is between loc1 and loc2
	 * @param toCheck - the {@link model.Location} to be check
	 * @param loc1 - the first {@link model.Location} bound
	 * @param loc2 - the second {@link model.Location} bound
	 * @return true if toCheck is in between loc1 and loc2, false otherweise
	 */
	protected boolean inBetween(Location toCheck, Location loc1, Location loc2)
	{
		if(!inSamePlane(toCheck, loc1, loc2))
			return false;
		if((toCheck.getRow() > loc1.getRow()) && (toCheck.getRow() < loc2.getRow()))
			return true;
		else if((toCheck.getRow() < loc1.getRow()) && (toCheck.getRow() > loc2.getRow()))
			return true;
		else if((toCheck.getCol() > loc1.getCol()) && (toCheck.getCol() < loc2.getCol()))
			return true;
		else if((toCheck.getCol() < loc1.getCol()) && (toCheck.getCol() > loc2.getCol()))
			return true;
		else
			return false;
	}
	/**
	 * This method serves the same purpose as inSmaePlane(Location loc1, Location loc2)
	 * @see that for more info
	 * @param loc1 - {@link model.Location} to be checked
	 * @param loc2 - {@link model.Location} to be checked
	 * @param loc3 - {@link model.Location} to be checked
	 * @return true if all Locations are in the same plane
	 */
	protected boolean inSamePlane(Location loc1, Location loc2, Location loc3)
	{
		if ((loc2.getRow() == loc3.getRow() && loc1.getRow() == loc2.getRow())
		|| (loc2.getCol() == loc3.getCol() && loc1.getCol() == loc2.getCol()))
			return true;
		int loc2Sum1 = Math.abs(loc2.getCol() - loc2.getRow());
		int loc3Sum1 = Math.abs(loc3.getCol() - loc3.getRow());
		int loc1Sum1 = Math.abs(loc1.getCol() - loc1.getRow());
		if (loc2Sum1 == loc3Sum1 && loc1Sum1 == loc2Sum1)
			return true;
		int loc2Sum2 = loc2.getCol() + loc2.getRow();
		int loc3Sum2 = loc3.getCol() + loc3.getRow();
		int loc1Sum2 = loc1.getCol() + loc1.getRow();
		if (loc2Sum2 == loc3Sum2 && loc1Sum2 == loc2Sum2)
			return true;
		return false;
	}
	/**
	 * This method determines if {@link model.Location} loc2 and loc2 are in the same plane (i.e row,
	 * column, or diagonal)
	 * @param loc1 - the first {@link model.Location} to be checked
	 * @param loc2 - the second {@link model.Location} to be checked
	 * @return true if the two {@link model.Location}s are in the same plane, false otherwise
	 */
	protected boolean inSamePlane(Location loc1, Location loc2)
	{
		if (loc1.getRow() == loc2.getRow() || loc1.getCol() == loc2.getCol())
			return true;
		int loc1Sum1 = Math.abs(loc1.getCol() - loc1.getRow());
		int loc2Sum1 = Math.abs(loc2.getCol() - loc2.getRow());
		if (loc1Sum1 == loc2Sum1)
			return true;
		int loc1Sum2 = loc1.getCol() + loc1.getRow();
		int loc2Sum2 = loc2.getCol() + loc2.getRow();
		if (loc1Sum2 == loc2Sum2)
			return true;
		return false;
	}
	/**
	 * This method returns a String representation of the grid.
	 * @return a String representing the grid.
	 */
	@Override
	public String toString()
	{
		String returned = "";
		for (int i = 0; i < rows; i++)
		{
			for (int i2 = 0; i2 < cols; i2++)
				returned += grid[i][i2] + " ";
			returned += "\n";
		}
		return returned;
	}
}
